/**
 * 
 */
package at.ac.lbg.media.vis.framework.service;

import java.util.ArrayList;
import java.util.List;

import no.uib.cipr.matrix.DenseMatrix;
import no.uib.cipr.matrix.EVD;
import no.uib.cipr.matrix.Matrix;
import no.uib.cipr.matrix.NotConvergedException;
import no.uib.cipr.matrix.sparse.CompDiagMatrix;
import no.uib.cipr.matrix.sparse.FlexCompColMatrix;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import at.ac.lbg.media.vis.framework.domain.ArtworkDO;
import at.ac.lbg.media.vis.framework.domain.CategoryDO;
import at.ac.lbg.media.vis.framework.persistence.ICategoryDAO;

/**
 * Multidimensional Scaling Service for Artworks.
 * @author Evelyn Münster
 *
 */
public class MDSService implements IMDSService {
	
	protected final Log logger = LogFactory.getLog(getClass());
	private List<CategoryDO> allCats;
	private ICategoryDAO categoryDAO;
	
	public MDSService() {}
	
	@Override
	public Matrix provideCoordinates(List<ArtworkDO> artworks) throws NotConvergedException, Exception {
		//long start = System.currentTimeMillis();
		provideCategoryVector(artworks, null);
		Matrix similarityM = generateObjectSimilarityMatrix(artworks);
		logger.debug("similarityM is set with:"+similarityM.numColumns()+"/"+similarityM.numRows());
		Matrix mds = generateClassicalMDS(similarityM);
		//long end = System.currentTimeMillis();
		//System.out.println("MDS execution time: "+(end-start)+" ms.");
		return mds;
	}
	
	@Override
	public Matrix provideCoordinates(List<ArtworkDO> artworks, List<CategoryDO> categories) throws NotConvergedException, Exception {
		//long start = System.currentTimeMillis();
		provideCategoryVector(artworks, categories);
		Matrix similarityM = generateObjectSimilarityMatrix(artworks);
		logger.debug("similarityM is set with:"+similarityM.numColumns()+"/"+similarityM.numRows());
		Matrix mds = generateClassicalMDS(similarityM);
		//long end = System.currentTimeMillis();
		//System.out.println("MDS execution time: "+(end-start)+" ms.");
       return mds;
	}
	
	private void provideCategoryVector(List<ArtworkDO> artworks, List<CategoryDO> cats) {
		for (ArtworkDO artwork : artworks) {
			provideCategoryVector(artwork, cats);
		}
	}
	
	/**
	 * Fills the vector attribute of an ArtworkDAO with the
	 * Category-Vector like {1,0,0,1,0,0,0,1,1,0,0,0,0,0,0,...}
	 * Size of array is number of keywords. Order of array
	 * is ascending order of Category#id.
	 * 1 = keyword is set
	 * 0 = keyword is not set
	 * @param artwork the artwork to be filled
	 * @param cats these keywords will be used for the vector. if null, all keywords will be
	 * fetched from db and used.
	 */
	private void provideCategoryVector(ArtworkDO artwork, List<CategoryDO> cats) {
		
		if (cats == null) {
		    if (allCats == null) {
		        allCats = categoryDAO.getKeywords();
		    }
		    cats = allCats;
		}
		List<Integer> vector = new ArrayList<Integer>(cats.size());
		List<CategoryDO> artworkCats = artwork.getCategories();
		boolean found = false;
		for (CategoryDO cat : cats) {
			for (CategoryDO awCat : artworkCats) {
				if (awCat.getId().intValue() == cat.getId().intValue()) {
					vector.add(1);
					found = true;
					break;
				}
			}
			if (!found) {
				vector.add(0);
			}
			found = false;
		}
		artwork.setVector(vector);
	}
	
	private Matrix generateClassicalMDS(Matrix similarityMatrix) throws NotConvergedException, Exception {
	
		//long start = System.currentTimeMillis();
		
//	// take city values instead for debugging
//		similarityMatrix = new FlexCompColMatrix(4, 4);
//		similarityMatrix.add(0, 0, 0);
//		similarityMatrix.add(0, 1, 112);
//		similarityMatrix.add(0, 2, 201);
//		similarityMatrix.add(0, 3, 184);
//		
//		similarityMatrix.add(1, 0, 112);
//		similarityMatrix.add(1, 1, 0);
//		similarityMatrix.add(1, 2, 196);
//		similarityMatrix.add(1, 3, 93);
//		
//		similarityMatrix.add(2, 0, 201);
//		similarityMatrix.add(2, 1, 196);
//		similarityMatrix.add(2, 2, 0);
//		similarityMatrix.add(2, 3, 141);
//		
//		similarityMatrix.add(3, 0, 184);
//		similarityMatrix.add(3, 1, 93);
//		similarityMatrix.add(3, 2, 141);
//		similarityMatrix.add(3, 3, 0);

		
		int dimension = similarityMatrix.numColumns();
		// square the similarity matrix. squaring means squaring each value, not the matrix... argl

//anika deleted sqrt at the end of similiarityMatrix calculation
//		for (int r = 0; r < dimension; r++) {
//			for (int c = 0; c < dimension; c++) {
//				double v = similarityMatrix.get(r, c);
//				similarityMatrix.set(r, c, Math.pow(v, 2));
//			}
//		}
		
		// Double centering start
		Matrix diagonal1 = new CompDiagMatrix(dimension, dimension);
		for (int i = 0; i < dimension; i++) {
			diagonal1.add(i, i, 1.0);
		}
		
		// A matrix filled with '1'
//		anika switched from FlexCompColMatrix to DenseMatrix
		Matrix just1s = new DenseMatrix(dimension, dimension);
		for (int r = 0; r < dimension; r++) {
			for (int c = 0; c < dimension; c++) {
				just1s.add(r, c, 1.0);
			}
		}
		
		double scaler = -(1.0 / dimension);
		Matrix j = just1s.scale(scaler);
		j = diagonal1.add(j);
		
		Matrix jhalf = j.copy();
		jhalf = jhalf.scale(-0.5);

		//long start3 = System.currentTimeMillis();
		//uses second most time of the algorithm
//		anika switched from FlexCompColMatrix to DenseMatrix
		Matrix interim = similarityMatrix.mult(j, new DenseMatrix(dimension, dimension));
		Matrix b = jhalf.mult(interim, new DenseMatrix(dimension, dimension));
		//long end3 = System.currentTimeMillis();
		//System.out.println("generateClassicalMDS before EVD execution time (start3/end3): "+(end3-start3)+" ms.");
		
		
		// Double centering end
		// calculate eigenvalues and eigenvectors of b
//		long start2 = System.currentTimeMillis();
//		uses most time of the algorithm
		EVD eigen = EVD.factorize(b);
//		long end2 = System.currentTimeMillis();
//		System.out.println("generateClassicalMDS after EVD execution time (start2/end2): "+(end2-start2)+" ms.");
		
		if (eigen == null || !eigen.hasRightEigenvectors()) {
			throw new Exception("Eigen decomposition failed.");	
		}
		// In many common applications, only right eigenvectors (and not left eigenvectors) 
		// need to be considered. Hence the unqualified term "eigenvector" can be understood
		// to refer to a right eigenvector.

		DenseMatrix eigenvectors = eigen.getRightEigenvectors();
		
		double[] eigenvalues = eigen.getRealEigenvalues();

		// Select the 2 top largest positive eigenvalues and their corresponding eigenvectors
		double eval1 = eigenvalues[0];
		double eval2 = eigenvalues[1];
		// make a diagonal matrix from the 2 eigenvalues DESC
		Matrix diagonalval = new FlexCompColMatrix(2, 2);
		diagonalval.add(0, 0, Math.pow(eval1, 0.5));
		diagonalval.add(1, 1, Math.pow(eval2, 0.5));
		
		Matrix evecs = new FlexCompColMatrix(eigenvectors.numRows(), 2);		
		for (int i = 0; i < eigenvectors.numRows(); i++) {
			evecs.add(i, 0, eigenvectors.get(i, 0));
			evecs.add(i, 1, eigenvectors.get(i, 1));
		}
		
		// Generate the final output coordinate matrix
//		anika
		Matrix mds = new FlexCompColMatrix(evecs.numRows(), diagonalval.numColumns());
//		Matrix mds = new DenseMatrix(evecs.numRows(), diagonalval.numColumns());// maybe helpful because it can differ...
		mds = evecs.mult(diagonalval, mds);
		
		logger.debug("mds is set with:"+mds.numColumns()+"/"+mds.numRows());
		//long end = System.currentTimeMillis();
		//System.out.println("generateClassicalMDS execution time: "+(end-start)+" ms.");
		return mds;
	}
	
	/**
	 * Use Euclidean Distance as measure for the similarity matrix.
	 * Computing the square root of the sum of the squares of the differences
	 * between corresponding values.
	 * @return
	 */
	private Matrix generateObjectSimilarityMatrix(List<ArtworkDO> artworks) {
		//long start = System.currentTimeMillis();
		int size = artworks.size();
		Matrix distanceMatrix = new FlexCompColMatrix(size, size);
		for (int i1 = 0; i1 < size; i1++) {
			ArtworkDO artwork1 = artworks.get(i1);
			List<Integer> vector1 = artwork1.getVector();
			for (int i2 = 0; i2 < size; i2++) {
				ArtworkDO artwork2 = artworks.get(i2);
				List<Integer> vector2 = artwork2.getVector();
//				double sum = 0;
				double distance = 0;
				for (int i = 0; i < vector1.size(); i++) {
					int x = vector1.get(i);
					int y = vector2.get(i);
					double square= Math.pow(x - y, 2);
//					sum += square;
					distance += square;
				}
				//anika sqrt(sum) is not important if you dont do the pow() in the MDS generation method
				//double distance = Math.sqrt(sum);
				distanceMatrix.add(i1, i2, distance);
			}
			
		}
		
//		logger.debug("finished generateObjectSimilarityMatrix size: "+distanceMatrix.numColumns()+"/"+distanceMatrix.numRows()+" of served "+size+" artworks");
//		long end = System.currentTimeMillis();
//		System.out.println("similarityMatrix execution time: "+(end-start)+" ms.");
		return distanceMatrix;
	}

	/**
	 * @param categoryDAO Dependency Injection
	 */
	public void setCategoryDAO(ICategoryDAO categoryDAO) {
		this.categoryDAO = categoryDAO;
	}
	
	

}
